﻿using System;
using System.Collections.Generic;
using MentalAlchemy.Atomics;
using MentalAlchemy.Molecules.MachineLearning;

namespace MentalAlchemy.Molecules.NevaAlgorithm.nevamod
{
	/// <summary>
	/// [molecule]
	/// 
	/// Operators notation:
	/// C -- add connection
	/// c -- remove connection
	/// N -- add node
	/// n -- remove node
	/// w -- change weight
	/// a -- change activation
	/// </summary>
	public class OperatorsAnalyzer
	{
		protected static Dictionary<string, string> opsNotation;
		public const string DEFAULT_CROSSOVER_CHAR = "x";
		public const string DEFAULT_ADD_CONNECTION_CHAR = "C";
		public const string DEFAULT_DEL_CONNECTION_CHAR = "c";
		public const string DEFAULT_ADD_NODE_CHAR = "N";
		public const string DEFAULT_DEL_NODE_CHAR = "n";
		public const string DEFAULT_MOD_WEIGHT_CHAR = "w";
		public const string DEFAULT_MOD_ACTIVATION_CHAR = "a";

		protected List<string> changes;
		protected List<NevaIndChanges> changes0;

		#region - Properties. -
		public List<NevaIndChanges> Changes
		{
			get
			{
				return changes0;
			}
			set
			{
				changes0 = value;
				changes = ConvertToStrings(changes0);
			}
		}

		public static Dictionary<string, string> OpsNotation
		{
			get { return opsNotation; }
		}

		/// <summary>
		/// Concatenated string for changes.
		/// </summary>
		public string ChangesString { get {return Strings.Concat(changes.ToArray());} }

		/// <summary>
		/// Returns all fitness delta values.
		/// </summary>
		public float[] FitnessDeltaValues
		{
			get { return GetFitnessDeltaValues(changes0); }
		}
		#endregion

		#region - Construction. -
		public OperatorsAnalyzer ()
		{
			opsNotation = new Dictionary<string, string>();
			opsNotation.Add(Operators.XOVER_NAME, DEFAULT_CROSSOVER_CHAR);
			opsNotation.Add(Operators.ADD_CONNECTION_MUTATION_NAME, DEFAULT_ADD_CONNECTION_CHAR);
			opsNotation.Add(Operators.DELETE_CONNECTION_MUTATION_NAME, DEFAULT_DEL_CONNECTION_CHAR);
			opsNotation.Add(Operators.ADD_NODE_MUTATION_NAME, DEFAULT_ADD_NODE_CHAR);
			opsNotation.Add(Operators.DELETE_NODE_MUTATION_NAME, DEFAULT_DEL_NODE_CHAR);
			opsNotation.Add(Operators.WEIGHT_MUTATION_NAME, DEFAULT_MOD_WEIGHT_CHAR);
			opsNotation.Add(Operators.NODE_ACTIVATION_MUTATION_NAME, DEFAULT_MOD_ACTIVATION_CHAR);
		}
		#endregion

		#region - Public static methods. -
		#region - Conversions. -
		/// <summary>
		/// [molecule]
		/// 
		/// Converts list of changes into the list of strings (for better processing).
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static List<string> ConvertToStrings(IList<NevaIndChanges> chs)
		{
			var res = new List<string>();
			foreach (var indChanges in chs)
			{
				res.Add(opsNotation[indChanges.OpName]);
			}
			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Converts given transitions data into matrix using specified operators ordering.
		/// </summary>
		/// <param name="trans"></param>
		/// <param name="opsOrder"></param>
		/// <returns></returns>
		public static float[,] TransitionsToMatrix(Dictionary<string, Dictionary<string, float>> trans, string[] opsOrder)
		{
			var size = opsOrder.Length;
			var res = new float[size, size];

			for (int i = 0; i < size; i++)
			{
				var opi = opsOrder[i];
				if (!trans.ContainsKey(opi))
				{
					// fill this line with zeroes.
					continue;
				}

				for (int j = 0; j < size; j++)
				{
					var opj = opsOrder[j];
					if (!trans[opi].ContainsKey(opj)) { continue; }
					res[j, i] = trans[opi][opj];	// trans[opi][opj] contains prob. of successfull trans from [opj] to [opi]
				}
			}

			return res;
		}
		#endregion

		#region - Stats. -
		/// <summary>
		/// [molecule]
		/// 
		/// Return max generation number, which is met in the provided array of changes.
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static int GetMaxGeneration(List<NevaIndChanges> chs)
		{
			var maxGen = 0;
			foreach (var ch in chs)
			{
				if (ch.Generation > maxGen) { maxGen = ch.Generation; }
			}
			return maxGen;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Calculates stats for fitness deltas (sum total/positives/negatives, count of positives/negatives/neutrals).
		/// </summary>
		/// <param name="chs"></param>
		public static Dictionary<string, Stats> CalculateFitnessDeltaStats(IList<NevaIndChanges> chs)
		{
			// 1. Split by different operations.
			var opChs = SplitByOperations(chs);

			// 2. Count fitness stats.
			var res = new Dictionary<string, Stats>();
			foreach (var pair in opChs)
			{
				res.Add(pair.Key, GetFitnessDeltaStats(pair.Value));
			}

			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Computes how operators utility changes over time.
		/// If some operator acts more than one time per generation its mean fitness change is computed.
		/// Note: All entries in a row are supposed to be ordered by their arrival time.
		/// NOte: All rows are sorted according to the operators' order given by Operators.GetOperators().
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static float[,] CalculateOpsTemporalSuccess(List<NevaIndChanges> chs)
		{
			// 0. get max generation value.
			var maxGen = GetMaxGeneration(chs);

			// 1. Split by different operations.
			var opChs = SplitByOperations(chs);

			// todo: decide format for the result.
			// todo: the problem is that when there's no info for the operator performance at generation t the table entry will be 0,
			// todo: which makes no difference when there's info, but operator's application yields no positive or negative result (just a pure zero).
			// todo: should I return NaN?

			var ops = Operators.GetOperators();
			var opsDic = new Dictionary<string, int>();
			var res = new float[ops.Length, maxGen + 1];
			var opsCount = new int[ops.Length, maxGen + 1];
			// init [res]
			for (int i = 0; i < ops.Length; i++)
			{
				var op = ops[i];
				opsDic.Add(op, i);
			}

			foreach (var ch in opChs)
			{
				// get idx.
				var idx = opsDic[ch.Key];

				foreach (var indChange in ch.Value)
				{
					res[idx, indChange.Generation] += indChange.FitDelta.Value;
					opsCount[idx, indChange.Generation]++;
				}
			}

			for (int i = 0; i < ops.Length; i++)
			{
				for (int j = 0; j <= maxGen; j++)
				{
					if (opsCount[i, j] <= 1) continue;	// it's either 0 or 1. Doesn't need to divide anyway.

					res[i, j] /= opsCount[i, j];
				}
			}

			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Returns stats for fitness deltas for the given array of changes.
		/// 
		/// The method suggests that all changes are made for the *same* operation!
		/// This is the main difference with [CalculateFitnessDeltaStats].
		/// 
		/// Note: The method uses only fitness values and doesn't consider any extras!
		/// Todo: Fix this.
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static Stats GetFitnessDeltaStats(IList<NevaIndChanges> chs)
		{
			var vals = new float[chs.Count];
			var gens = new int[chs.Count];
			for (int i = 0; i < vals.Length; i++)
			{
				vals[i] = chs[i].FitDelta.Value;
				gens[i] = chs[i].Generation;
			}

			// calculate fitness stats.
			var res = VectorMath.CalculateStats(vals);

			// calculate additional stats
			//var gStats = VectorMath.CalculateStats(gens);
			//res.AppendData(gStats.Mean, "Mean Generation");
			//res.AppendData(gStats.Min, "Min Generation");
			//res.AppendData(gStats.Max, "Max Generation");
			//res.AppendData(gStats.Median, "Median Generation");
			//res.AppendData(gStats.Variance, "Generation Var");

			// extract neutral results.
			var neut = new List<float>();
			var ngens = new List<int>();
			var nonneut = new List<float>();
			var nonngens = new List<int>();
			var eps = 1e-6f;
			for (int i = 0; i < vals.Length; i++)
			{
				var val = vals[i];

				if (Math.Abs(val) < eps)
				{
					neut.Add(val);
					ngens.Add(chs[i].Generation);
				}
				else
				{
					nonneut.Add(val);
					nonngens.Add(chs[i].Generation);
				}
			}

			// compute stats for neutral and non-neutral results.
			Stats gStats;
			res.AppendData(neut.Count, "(N) Count");
			gStats = VectorMath.CalculateStats(ngens.ToArray());
			res.AppendData(gStats.Mean, "(N) Mean Generation");
			res.AppendData(gStats.Median, "(N) Median Generation");
			res.AppendData(gStats.Variance, "(N) Generation Var");
			//if (ngens.Count > 0)
			//{
			//    gStats = VectorMath.CalculateStats(ngens.ToArray());
			//    res.AppendData(gStats.Mean, "(N) Mean Generation");
			//    res.AppendData(gStats.Variance, "(N) Generation Var");
			//}
			//else
			//{
			//    res.AppendData(0, "(N) Mean Generation");
			//    res.AppendData(0, "(N) Generation Var");
			//}

			res.AppendData(nonneut.Count, "(non-N) Count");
			gStats = VectorMath.CalculateStats(nonngens.ToArray());
			res.AppendData(gStats.Mean, "(non-N) Mean Generation");
			res.AppendData(gStats.Median, "(N) Median Generation");
			res.AppendData(gStats.Variance, "(non-N) Generation Var");

			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Computes succesfull transitions rate for each operator.
		/// Each row contains successfull 'transitions' rate from different operators.
		/// </summary>
		/// <param name="chs">List of changes.</param>
		/// <param name="minimization">Indicates whether minimization or maximization problem was considered.</param>
		/// <returns></returns>
		public static Dictionary<string, Dictionary<string, float>> ComputeSuccessfulTransitions(IList<NevaIndChanges> chs, bool minimization)
		{
			var res = new Dictionary<string, Dictionary<string, float>>();
			var counts = new Dictionary<string, Dictionary<string, int>>();

			//
			// Init res.
			var ops = Operators.GetOperators();
			foreach (var op in ops)
			{
				res.Add(op, new Dictionary<string, float>());
				counts.Add(op, new Dictionary<string, int>());
			}

			//
			// Compute total and successfull counts.
			for (int i = 1; i < chs.Count; i++)
			{
				if ((minimization && chs[i].FitDelta.Value < 0)
					||
					(!minimization && chs[i].FitDelta.Value > 0))
				{	// fitness delta value was successfull.

					if (!res[chs[i].OpName].ContainsKey(chs[i - 1].OpName))
					{
						res[chs[i].OpName].Add(chs[i - 1].OpName, 0);
					}
					res[chs[i].OpName][chs[i - 1].OpName]++;	// count this event.
				}

				// compute total number of transitions.
				if (!counts[chs[i].OpName].ContainsKey(chs[i - 1].OpName))
				{
					counts[chs[i].OpName].Add(chs[i - 1].OpName, 0);
				}
				counts[chs[i].OpName][chs[i - 1].OpName]++;
			}

			//
			// Compute rates.
			foreach (var pair in res)
			{
				var keys = new string[pair.Value.Count];
				pair.Value.Keys.CopyTo(keys, 0);
				//var vals = new float[pair.Value.Count];
				//pair.Value.Values.CopyTo(vals, 0);

				for (int i = 0; i < pair.Value.Count; i++)
				{
					if (counts[pair.Key][keys[i]] == 0) continue;

					pair.Value[keys[i]] /= counts[pair.Key][keys[i]];
				}
				//foreach (var trans in pair.Value)
				//{
				//    if (counts[pair.Key][trans.Key] == 0) continue;

				//    pair.Value[trans.Key] /= counts[ pair.Key ][ trans.Key ];
				//}
			}

			return res;
		}
		#endregion

		#region - Access. -
		/// <summary>
		/// [molecule]
		/// 
		/// Returns fitness deltas for the given array of individual changes.
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static float[] GetFitnessDeltaValues(List<NevaIndChanges> chs)
		{
			var res = new float[chs.Count];
			for (int i = 0; i < res.Length; i++)
			{
				res[i] = chs[i].FitDelta.Value;
			}
			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Returns information about generations where different operators acted.
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static Dictionary<string, int[]> GetGenerationsData(IList<NevaIndChanges> chs)
		{
			var ops = SplitByOperations(chs);
			var res = new Dictionary<string, int[]>();

			foreach (var opData in ops)
			{
				var count = opData.Value.Count;
				if (count == 0) continue;

				var temp = new int[count];
				for (int i = 0; i < count; i++)
				{
					temp[i] = opData.Value[i].Generation;
				}
				res.Add(opData.Key, temp);
			}

			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Splits given array of changes into subarrays by operations.
		/// </summary>
		/// <param name="chs"></param>
		/// <returns></returns>
		public static Dictionary<string, List<NevaIndChanges>> SplitByOperations(IList<NevaIndChanges> chs)
		{
			var res = new Dictionary<string, List<NevaIndChanges>>();

			foreach (var ch in chs)
			{
				if (!res.ContainsKey(ch.OpName))
				{
					res.Add(ch.OpName, new List<NevaIndChanges>());
				}
				res[ch.OpName].Add(ch);
			}

			return res;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Removes all individual changes that occured after the given generation.
		/// </summary>
		/// <param name="chs"></param>
		/// <param name="gen"></param>
		/// <returns></returns>
		public static List<NevaIndChanges> TrimByGeneration(List<NevaIndChanges> chs, int gen)
		{
			var res = new List<NevaIndChanges>();

			foreach (var ch in chs)
			{
				if (ch.Generation <= gen)
				{
					res.Add(ch);
				}
			}

			return res;
		}
		#endregion
		#endregion
	}
}